/*
 * Copyright (c) 2022 DuckDuckGo
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.duckduckgo.autofill.impl

import android.webkit.WebView
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
import com.duckduckgo.autofill.api.Callback
import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType
import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector
import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.NoMatch
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.api.domain.app.LoginTriggerType
import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor
import com.duckduckgo.autofill.impl.AutofillStoredBackJavascriptInterface.UrlProvider
import com.duckduckgo.autofill.impl.configuration.AutofillAvailableInputTypesProvider
import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator
import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextRecentInstallChecker
import com.duckduckgo.autofill.impl.email.incontext.store.EmailProtectionInContextDataStore
import com.duckduckgo.autofill.impl.importing.InBrowserImportPromo
import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster
import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest
import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser
import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataCredentialsRequest
import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataRequest
import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.FORM_SUBMISSION
import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.PARTIAL_SAVE
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputMainType.CREDENTIALS
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.PASSWORD
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.USERNAME
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.USER_INITIATED
import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter
import com.duckduckgo.autofill.impl.partialsave.PartialCredentialSaveStore
import com.duckduckgo.autofill.impl.partialsave.UsernameBackFiller
import com.duckduckgo.autofill.impl.partialsave.UsernameBackFiller.BackFillResult.BackFillNotSupported
import com.duckduckgo.autofill.impl.partialsave.UsernameBackFiller.BackFillResult.BackFillSupported
import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository
import com.duckduckgo.autofill.impl.systemautofill.SystemAutofillServiceSuppressor
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.UpdateMatchingUsernames
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.AutogeneratedPasswordEventResolver
import com.duckduckgo.autofill.noopDeduplicator
import com.duckduckgo.common.test.CoroutineTestRule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class AutofillStoredBackJavascriptInterfaceTest {

    @get:Rule
    var coroutineRule = CoroutineTestRule()

    private val requestParser: AutofillRequestParser = mock()
    private val autofillStore: InternalAutofillStore = mock()
    private val autofillMessagePoster: AutofillMessagePoster = mock()
    private val autofillResponseWriter: AutofillResponseWriter = mock()
    private val currentUrlProvider: UrlProvider = mock()
    private val autofillCapabilityChecker: AutofillCapabilityChecker = mock()
    private val passwordEventResolver: AutogeneratedPasswordEventResolver = mock()
    private val testSavedLoginsMonitor: AutomaticSavedLoginsMonitor = mock()
    private val coroutineScope: CoroutineScope = TestScope()
    private val shareableCredentials: ShareableCredentials = mock()
    private val emailManager: EmailManager = mock()
    private val inContextDataStore: EmailProtectionInContextDataStore = mock()
    private val recentInstallChecker: EmailProtectionInContextRecentInstallChecker = mock()
    private val testWebView = WebView(getApplicationContext())
    private val loginDeduplicator: AutofillLoginDeduplicator = noopDeduplicator()
    private val systemAutofillServiceSuppressor: SystemAutofillServiceSuppressor = mock()
    private val neverSavedSiteRepository: NeverSavedSiteRepository = mock()
    private val partialCredentialSaveStore: PartialCredentialSaveStore = mock()
    private val usernameBackFiller: UsernameBackFiller = mock()
    private val existingCredentialMatchDetector: ExistingCredentialMatchDetector = mock()
    private val inBrowserImportPromo: InBrowserImportPromo = mock()
    private val autofillAvailableInputTypesProvider: AutofillAvailableInputTypesProvider = mock()
    private lateinit var testee: AutofillStoredBackJavascriptInterface

    private val testCallback = TestCallback()

    @Before
    fun setUp() = runTest {
        whenever(autofillCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(true)
        whenever(autofillCapabilityChecker.canInjectCredentialsToWebView(any())).thenReturn(true)
        whenever(autofillCapabilityChecker.canSaveCredentialsFromWebView(any())).thenReturn(true)
        whenever(shareableCredentials.shareableCredentials(any())).thenReturn(emptyList())
        whenever(neverSavedSiteRepository.isInNeverSaveList(any())).thenReturn(false)
        testee = AutofillStoredBackJavascriptInterface(
            requestParser = requestParser,
            autofillStore = autofillStore,
            shareableCredentials = shareableCredentials,
            autofillMessagePoster = autofillMessagePoster,
            autofillResponseWriter = autofillResponseWriter,
            coroutineScope = coroutineScope,
            dispatcherProvider = coroutineRule.testDispatcherProvider,
            currentUrlProvider = currentUrlProvider,
            autofillCapabilityChecker = autofillCapabilityChecker,
            passwordEventResolver = passwordEventResolver,
            emailManager = emailManager,
            inContextDataStore = inContextDataStore,
            recentInstallChecker = recentInstallChecker,
            loginDeduplicator = loginDeduplicator,
            systemAutofillServiceSuppressor = systemAutofillServiceSuppressor,
            neverSavedSiteRepository = neverSavedSiteRepository,
            partialCredentialSaveStore = partialCredentialSaveStore,
            usernameBackFiller = usernameBackFiller,
            existingCredentialMatchDetector = existingCredentialMatchDetector,
            inBrowserImportPromo = inBrowserImportPromo,
        )
        testee.callback = testCallback
        testee.webView = testWebView
        testee.autoSavedLoginsMonitor = testSavedLoginsMonitor

        whenever(partialCredentialSaveStore.getUsernameForBackFilling(any())).thenReturn(null)
        whenever(usernameBackFiller.isBackFillingUsernameSupported(anyOrNull(), any())).thenReturn(BackFillNotSupported)
        whenever(existingCredentialMatchDetector.determine(anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(NoMatch)
        whenever(currentUrlProvider.currentUrl(testWebView)).thenReturn(EXAMPLE_DOT_COM_URL)
        whenever(requestParser.parseAutofillDataRequest(any())).thenReturn(
            Result.success(AutofillDataRequest(CREDENTIALS, USERNAME, USER_INITIATED, null)),
        )
        whenever(autofillResponseWriter.generateEmptyResponseGetAutofillData()).thenReturn("")
        whenever(autofillResponseWriter.generateResponseGetAutofillData(any())).thenReturn("")
        whenever(inBrowserImportPromo.canShowPromo(any(), anyOrNull())).thenReturn(false)
        whenever(autofillStore.getCredentials(any())).thenReturn(emptyList())
    }

    @Test
    fun whenInjectingNoCredentialResponseThenCorrectJsonWriterInvoked() = runTest {
        testee.injectNoCredentials()
        verify(autofillResponseWriter).generateEmptyResponseGetAutofillData()
        verifyMessageSent()
    }

    @Test
    fun whenInjectingCredentialResponseThenCorrectJsonWriterInvoked() = runTest {
        val loginCredentials = LoginCredentials(0, "example.com", "username", "password")
        testee.injectCredentials(loginCredentials)
        verify(autofillResponseWriter).generateResponseGetAutofillData(any())
        verifyMessageSent()
    }

    @Test
    fun whenGetAutofillDataCalledNoCredentialsAvailableThenNoCredentialsCallbackInvoked() = runTest {
        setupRequestForSubTypeUsername()
        whenever(autofillStore.getCredentials(any())).thenReturn(emptyList())
        initiateGetAutofillDataRequest()
        assertCredentialsUnavailable()
    }

    @Test
    fun whenGetAutofillDataCalledWithCredentialsAvailableThenCredentialsAvailableCallbackInvoked() = runTest {
        whenever(autofillStore.getCredentials(any())).thenReturn(listOf(LoginCredentials(0, "example.com", "username", "password")))
        initiateGetAutofillDataRequest()
        assertCredentialsAvailable()
    }

    @Test
    fun whenGetAutofillDataCalledWithCredentialsAvailableWithNullUsernameUsernameConvertedToEmptyString() = runTest {
        setupRequestForSubTypePassword()
        whenever(autofillStore.getCredentials(any())).thenReturn(
            listOf(
                loginCredential(username = null, password = "foo"),
                loginCredential(username = null, password = "bar"),
                loginCredential(username = "foo", password = "bar"),
            ),
        )
        initiateGetAutofillDataRequest()
        assertCredentialsAvailable()

        // ensure the list of credentials now has two entries with empty string username (one for each null username)
        assertCredentialsContains({ it.username }, "", "")
    }

    @Test
    fun whenRequestSpecifiesSubtypeUsernameAndNoEntriesThenNoCredentialsCallbackInvoked() = runTest {
        setupRequestForSubTypeUsername()
        whenever(autofillStore.getCredentials(any())).thenReturn(emptyList())
        initiateGetAutofillDataRequest()
        assertCredentialsUnavailable()
    }

    @Test
    fun whenRequestSpecifiesSubtypeUsernameAndNoEntriesWithAUsernameThenNoCredentialsCallbackInvoked() = runTest {
        setupRequestForSubTypeUsername()
        whenever(autofillStore.getCredentials(any())).thenReturn(
            listOf(
                loginCredential(username = null, password = "foo"),
                loginCredential(username = null, password = "bar"),
            ),
        )
        initiateGetAutofillDataRequest()
        assertCredentialsUnavailable()
    }

    @Test
    fun whenRequestSpecifiesSubtypeUsernameAndSingleEntryWithAUsernameThenCredentialsAvailableCallbackInvoked() = runTest {
        setupRequestForSubTypeUsername()
        whenever(autofillStore.getCredentials(any())).thenReturn(
            listOf(
                loginCredential(username = null, password = "foo"),
                loginCredential(username = null, password = "bar"),
                loginCredential(username = "foo", password = "bar"),
            ),
        )
        initiateGetAutofillDataRequest()
        assertCredentialsAvailable()
        assertCredentialsContains({ it.username }, "foo")
    }

    @Test
    fun whenRequestSpecifiesSubtypeUsernameAndMultipleEntriesWithAUsernameThenCredentialsAvailableCallbackInvoked() = runTest {
        setupRequestForSubTypeUsername()
        whenever(autofillStore.getCredentials(any())).thenReturn(
            listOf(
                loginCredential(username = null, password = "foo"),
                loginCredential(username = "username1", password = "bar"),
                loginCredential(username = null, password = "bar"),
                loginCredential(username = null, password = "bar"),
                loginCredential(username = "username2", password = null),
            ),
        )
        initiateGetAutofillDataRequest()
        assertCredentialsAvailable()
        assertCredentialsContains({ it.username }, "username1", "username2")
    }

    @Test
    fun whenRequestSpecifiesSubtypePasswordAndNoEntriesThenNoCredentialsCallbackInvoked() = runTest {
        setupRequestForSubTypePassword()
        whenever(autofillStore.getCredentials(any())).thenReturn(emptyList())
        initiateGetAutofillDataRequest()
        assertCredentialsUnavailable()
    }

    @Test
    fun whenRequestSpecifiesSubtypePasswordAndNoEntriesWithAPasswordThenNoCredentialsCallbackInvoked() = runTest {
        setupRequestForSubTypePassword()
        whenever(autofillStore.getCredentials(any())).thenReturn(
            listOf(
                loginCredential(username = "foo", password = null),
                loginCredential(username = "bar", password = null),
            ),
        )
        initiateGetAutofillDataRequest()
        assertCredentialsUnavailable()
    }

    @Test
    fun whenRequestSpecifiesSubtypePasswordAndSingleEntryWithAPasswordThenCredentialsAvailableCallbackInvoked() = runTest {
        setupRequestForSubTypePassword()
        whenever(autofillStore.getCredentials(any())).thenReturn(
            listOf(
                loginCredential(username = null, password = null),
                loginCredential(username = "foobar", password = null),
                loginCredential(username = "foo", password = "bar"),
            ),
        )
        initiateGetAutofillDataRequest()
        assertCredentialsAvailable()
        assertCredentialsContains({ it.password }, "bar")
    }

    @Test
    fun whenRequestSpecifiesSubtypePasswordAndMultipleEntriesWithAPasswordThenCredentialsAvailableCallbackInvoked() = runTest {
        setupRequestForSubTypePassword()
        whenever(autofillStore.getCredentials(any())).thenReturn(
            listOf(
                loginCredential(username = null, password = null),
                loginCredential(username = "username2", password = null),
                loginCredential(username = "username1", password = "password1"),
                loginCredential(username = null, password = "password2"),
                loginCredential(username = null, password = "password3"),

            ),
        )
        initiateGetAutofillDataRequest()
        assertCredentialsAvailable()
        assertCredentialsContains({ it.password }, "password1", "password2", "password3")
    }

    @Test
    fun whenStoreFormDataCalledWithNoUsernameThenCallbackInvoked() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = null, password = "password")
        testee.storeFormData("")
        assertNotNull(testCallback.credentialsToSave)
    }

    @Test
    fun whenStoreFormDataCalledWithNoPasswordThenCallbackInvoked() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = "dax@duck.com", password = null)
        testee.storeFormData("")
        assertNotNull(testCallback.credentialsToSave)
        assertEquals("dax@duck.com", testCallback.credentialsToSave!!.username)
    }

    @Test
    fun whenStoreFormDataCalledWithNullUsernameAndPasswordThenCallbackNotInvoked() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = null, password = null)
        testee.storeFormData("")
        assertNull(testCallback.credentialsToSave)
    }

    @Test
    fun whenStoreFormDataCalledWithBlankUsernameThenCallbackInvoked() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = " ", password = "password")
        testee.storeFormData("")
        assertEquals(" ", testCallback.credentialsToSave!!.username)
        assertEquals("password", testCallback.credentialsToSave!!.password)
    }

    @Test
    fun whenStoreFormDataCalledWithBlankPasswordThenCallbackInvoked() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = "username", password = " ")
        testee.storeFormData("")
        assertEquals("username", testCallback.credentialsToSave!!.username)
        assertEquals(" ", testCallback.credentialsToSave!!.password)
    }

    @Test
    fun whenStoreFormDataCalledButSiteInNeverSaveListThenCallbackNotInvoked() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = "username", password = "password")
        whenever(neverSavedSiteRepository.isInNeverSaveList(any())).thenReturn(true)
        testee.storeFormData("")
        assertNull(testCallback.credentialsToSave)
    }

    @Test
    fun whenStoreFormDataCalledWithBlankUsernameAndBlankPasswordThenCallbackNotInvoked() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = " ", password = " ")
        testee.storeFormData("")
        assertNull(testCallback.credentialsToSave)
    }

    @Test
    fun whenStoreFormDataCalledAndParsingErrorThenExceptionIsContained() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = "username", password = "password")
        whenever(requestParser.parseStoreFormDataRequest(any())).thenReturn(Result.failure(RuntimeException("Parsing error")))
        testee.storeFormData("")
        assertNull(testCallback.credentialsToSave)
    }

    @Test
    fun whenPartialFormSubmissionWithUsernameThenPartialSavePersisted() = runTest {
        val username = "user"
        val url = "https://example.com"
        configureRequestParserToReturnPartialSaveRequestType(username = username)
        whenever(currentUrlProvider.currentUrl(anyOrNull())).thenReturn(url)
        testee.storeFormData("")
        verify(partialCredentialSaveStore).saveUsername(url = eq(url), username = eq(username))
    }

    @Test
    fun whenPartialFormSubmissionWithNoUsernameThenPartialSaveNotPersisted() = runTest {
        configureRequestParserToReturnPartialSaveRequestType(username = null)
        testee.storeFormData("")
        verifyNoInteractions(partialCredentialSaveStore)
    }

    @Test
    fun whenFormSubmissionMissingUsernameAfterPartialSubmissionGaveUsernameThenUsernameBackFilled() = runTest {
        whenever(usernameBackFiller.isBackFillingUsernameSupported(anyOrNull(), any())).thenReturn(
            BackFillSupported(username = "username-from-backfill"),
        )
        configureRequestParserToReturnSaveCredentialRequestType(username = null, password = "password")

        testee.storeFormData("")
        assertEquals("username-from-backfill", testCallback.credentialsToSave!!.username)
    }

    @Test
    fun whenFormSubmissionHasUsernameAfterPartialSubmissionGaveUsernameThenFormSubmissionUsernameUsed() = runTest {
        whenever(partialCredentialSaveStore.getUsernameForBackFilling(any())).thenReturn("username-from-backfill")
        configureRequestParserToReturnSaveCredentialRequestType(username = "username-from-form-submission", password = "password")

        testee.storeFormData("")
        assertEquals("username-from-form-submission", testCallback.credentialsToSave!!.username)
    }

    @Test
    fun whenUpdateRequiredOnMatchingUsernamesAndLoginStoredWithEmptyPasswordThenIsUpdated() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = "test", password = "password")
        configureActionsToBeProcessed(listOf(UpdateMatchingUsernames("test")))

        listOf(loginCredential(username = "test", password = "")).returnTheseForUrl(EXAMPLE_DOT_COM_URL)
        EXAMPLE_DOT_COM_URL.configureUpdateSuccessfulForUrl()

        testee.storeFormData("")
        verify(autofillStore).updateCredentials(eq(EXAMPLE_DOT_COM_URL), any(), eq(CredentialUpdateType.Password))
        assertTrue(testCallback.onCredentialsSavedCalled)
    }

    @Test
    fun whenUpdateRequiredOnMatchingUsernamesAndLoginStoredWithNullPasswordThenIsUpdated() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = "test", password = "new-password")
        configureActionsToBeProcessed(listOf(UpdateMatchingUsernames("test")))

        listOf(loginCredential(username = "test", password = null)).returnTheseForUrl(EXAMPLE_DOT_COM_URL)
        EXAMPLE_DOT_COM_URL.configureUpdateSuccessfulForUrl()

        testee.storeFormData("")
        verify(autofillStore).updateCredentials(
            eq(EXAMPLE_DOT_COM_URL),
            eq(loginCredential(username = "test", password = "new-password")),
            eq(CredentialUpdateType.Password),
        )
        assertTrue(testCallback.onCredentialsSavedCalled)
    }

    @Test
    fun whenUpdateRequiredOnMatchingUsernamesAndLoginsStoredOnlySomeMatchCriteriaThenMatchingOnesUpdated() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = "test", password = "password")
        configureActionsToBeProcessed(listOf(UpdateMatchingUsernames("test")))

        listOf(
            loginCredential(username = "test", password = null),
            loginCredential(username = "test", password = ""),
            loginCredential(username = "test", password = "non-empty"),
            loginCredential(username = "different-username", password = ""),
        ).returnTheseForUrl(EXAMPLE_DOT_COM_URL)
        EXAMPLE_DOT_COM_URL.configureUpdateSuccessfulForUrl()

        testee.storeFormData("")
        verify(autofillStore, times(2)).updateCredentials(eq(EXAMPLE_DOT_COM_URL), any(), eq(CredentialUpdateType.Password))
        assertTrue(testCallback.onCredentialsSavedCalled)
    }

    @Test
    fun whenUpdateRequiredOnMatchingUsernamesAndLoginStoredWithNonEmptyPasswordThenIsNotUpdated() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = "test", password = "password")
        configureActionsToBeProcessed(listOf(UpdateMatchingUsernames("test")))

        val matchingLogins = listOf(loginCredential(username = "test", password = "non-empty"))
        matchingLogins.returnTheseForUrl(EXAMPLE_DOT_COM_URL)
        EXAMPLE_DOT_COM_URL.configureUpdateSuccessfulForUrl()

        testee.storeFormData("")
        verify(autofillStore, never()).updateCredentials(eq(EXAMPLE_DOT_COM_URL), any(), eq(CredentialUpdateType.Password))
        assertFalse(testCallback.onCredentialsSavedCalled)
    }

    @Test
    fun whenUpdateRequiredOnMatchingUsernamesAndLoginStoredWithDifferentUsernameThenIsNotUpdated() = runTest {
        configureRequestParserToReturnSaveCredentialRequestType(username = "test", password = "password")
        configureActionsToBeProcessed(listOf(UpdateMatchingUsernames("test")))

        val matchingLogins = listOf(loginCredential(username = "different-username", password = ""))
        matchingLogins.returnTheseForUrl(EXAMPLE_DOT_COM_URL)
        EXAMPLE_DOT_COM_URL.configureUpdateSuccessfulForUrl()

        testee.storeFormData("")
        verify(autofillStore, never()).updateCredentials(eq(EXAMPLE_DOT_COM_URL), any(), eq(CredentialUpdateType.Password))
        assertFalse(testCallback.onCredentialsSavedCalled)
    }

    @Test
    fun whenUserShouldBePromptedToImportPasswordsCallbackIsInvoked() = runTest {
        whenever(inBrowserImportPromo.canShowPromo(any(), anyOrNull())).thenReturn(true)
        initiateGetAutofillDataRequest()
        assertImportPasswordPrompted()
    }

    @Test
    fun whenUserShouldNotBePromptedToImportPasswordsBecauseImportRulesExcludeItThenCallbackIsNotInvoked() = runTest {
        whenever(inBrowserImportPromo.canShowPromo(any(), anyOrNull())).thenReturn(false)
        initiateGetAutofillDataRequest()
        assertImportPasswordNotPrompted()
    }

    @Test
    fun whenUserShouldBeNotPromptedToImportPasswordsBecauseThereArePageCredentialsCallbackIsNotInvoked() = runTest {
        whenever(inBrowserImportPromo.canShowPromo(any(), anyOrNull())).thenReturn(true)
        whenever(autofillStore.getCredentials(any())).thenReturn(listOf(LoginCredentials(0, "example.com", "username", "password")))
        initiateGetAutofillDataRequest()
        assertImportPasswordNotPrompted()
        assertCredentialsAvailable()
    }

    private suspend fun configureActionsToBeProcessed(actions: List<Actions>) {
        whenever(
            passwordEventResolver.decideActions(
                anyOrNull(),
                any(),
                eq(NoMatch),
                any(),
                anyOrNull(),
            ),
        ).thenReturn(actions)
    }

    private suspend fun List<LoginCredentials>.returnTheseForUrl(url: String) {
        whenever(autofillStore.getCredentials(url)).thenReturn(this)
    }

    private suspend fun String.configureUpdateSuccessfulForUrl() {
        whenever(autofillStore.updateCredentials(eq(this), org.mockito.kotlin.any(), eq(CredentialUpdateType.Password))).thenReturn(loginCredential())
    }

    private suspend fun configureRequestParserToReturnSaveCredentialRequestType(
        username: String?,
        password: String?,
    ) {
        val credentials = AutofillStoreFormDataCredentialsRequest(username = username, password = password)
        val topLevelRequest = AutofillStoreFormDataRequest(credentials, FORM_SUBMISSION)
        whenever(requestParser.parseStoreFormDataRequest(any())).thenReturn(Result.success(topLevelRequest))
        whenever(passwordEventResolver.decideActions(anyOrNull(), any(), eq(NoMatch), any(), anyOrNull())).thenReturn(listOf(Actions.PromptToSave))
    }

    private suspend fun configureRequestParserToReturnPartialSaveRequestType(username: String?) {
        val credentials = AutofillStoreFormDataCredentialsRequest(username = username, password = null)
        val topLevelRequest = AutofillStoreFormDataRequest(credentials, PARTIAL_SAVE)
        whenever(requestParser.parseStoreFormDataRequest(any())).thenReturn(Result.success(topLevelRequest))
    }

    private fun assertCredentialsContains(
        property: (LoginCredentials) -> String?,
        vararg expected: String?,
    ) {
        val numberExpected = expected.size
        val numberMatched = testCallback.credentialsToInject?.filter { expected.contains(property(it)) }?.count()
        assertEquals("Wrong number of matched properties. Expected $numberExpected but found $numberMatched", numberExpected, numberMatched)
    }

    private fun loginCredential(
        username: String? = null,
        password: String? = null,
    ) = LoginCredentials(0, "example.com", username, password)

    private suspend fun setupRequestForSubTypeUsername() {
        whenever(requestParser.parseAutofillDataRequest(any())).thenReturn(
            Result.success(AutofillDataRequest(CREDENTIALS, USERNAME, USER_INITIATED, null)),
        )
    }

    private suspend fun setupRequestForSubTypePassword() {
        whenever(requestParser.parseAutofillDataRequest(any())).thenReturn(
            Result.success(AutofillDataRequest(CREDENTIALS, PASSWORD, USER_INITIATED, null)),
        )
    }

    private fun assertCredentialsUnavailable() {
        assertNotNull("Callback has not been called", testCallback.credentialsAvailableToInject)
        assertFalse(testCallback.credentialsAvailableToInject!!)
    }

    private fun assertCredentialsAvailable() {
        assertNotNull("Callback has not been called", testCallback.credentialsAvailableToInject)
        assertTrue(testCallback.credentialsAvailableToInject!!)
    }

    private fun assertImportPasswordPrompted() {
        assertTrue(testCallback.onPromptedToImportPassword)
    }

    private fun assertImportPasswordNotPrompted() {
        assertFalse(testCallback.onPromptedToImportPassword)
    }

    private fun initiateGetAutofillDataRequest() {
        testee.getAutofillData("")
    }

    private suspend fun verifyMessageSent() {
        verify(autofillMessagePoster).postMessage(any(), anyOrNull())
    }

    class TestCallback : Callback {

        // for injection
        var credentialsToInject: List<LoginCredentials>? = null
        var credentialsAvailableToInject: Boolean? = null

        // for saving
        var credentialsToSave: LoginCredentials? = null

        // for password generation
        var offeredToGeneratePassword: Boolean = false

        var onCredentialsSavedCalled: Boolean = false

        var onPromptedToImportPassword: Boolean = false

        override suspend fun onCredentialsAvailableToInject(
            originalUrl: String,
            credentials: List<LoginCredentials>,
            triggerType: LoginTriggerType,
        ) {
            credentialsAvailableToInject = true
            this.credentialsToInject = credentials
        }

        override suspend fun onCredentialsAvailableToSave(
            currentUrl: String,
            credentials: LoginCredentials,
        ) {
            credentialsToSave = credentials
        }

        override suspend fun onGeneratedPasswordAvailableToUse(
            originalUrl: String,
            username: String?,
            generatedPassword: String,
        ) {
            offeredToGeneratePassword = true
        }

        override fun noCredentialsAvailable(originalUrl: String) {
            credentialsAvailableToInject = false
        }

        override fun onCredentialsSaved(savedCredentials: LoginCredentials) {
            onCredentialsSavedCalled = true
        }

        override suspend fun promptUserToImportPassword(originalUrl: String) {
            onPromptedToImportPassword = true
        }
    }

    private companion object {
        private const val EXAMPLE_DOT_COM_URL = "https://example.com"
    }
}
